//
// https://developer.bitcoin.org/devguide/wallet.html
//
// Wallet programs create public keys to receive satoshis
// and use the corresponding private keys to spend those satoshis.
//
// Wallet files store private keys and (optionally) other information
// related to transactions for the wallet program.
//
// Wallet programs also need to interact with the peer-to-peer network
// to get information from the block chain and to broadcast new transactions.
//
// This leaves us with three necessary, but separable, parts of a wallet system:
// a public key distribution program, a signing program, and a networked program.
//
// we speak about distributing public keys generically.
// In many cases, P2PKH or P2SH hashes will be distributed instead of public keys,
// with the actual public keys only being distributed
// when the outputs they control are spent.
//
// The simplest wallet is a program which performs all three functions:
//      it generates private keys, derives the corresponding public keys, helps distribute those public keys as necessary,
//      monitors for outputs spent to those public keys,
//      creates and signs transactions spending those outputs, and broadcasts the signed transactions.
//

use std::{path::Path, sync::Arc};

use tokio::sync::Mutex;

use bc_base::{
    PrivateKeyBytes, PublicKeyBytes, PublicKeyHashBytes, TransactionId, TransactionValue,
    block::Block,
    transaction::{Transaction, TransactionOut},
    wallet::wallet_tx::WalletTx,
    wallet::{ItemNumberInWalletDb, wallet_cache::WalletCache},
};
use bc_consensus::Consensus;

use crate::WalletsError;
use crate::wallet_storage_with_cache::WalletStorageWithCache;

// store below info to db
// 1. accounts
//  (pubkey, privkey)
//  (pubkey-hash, name)
//  (pubkey, wallet-key) // TODO: class CWalletKey
//  default-key
// 2. utxos
//  (tx-id, tx)
// 3. misc
//  version

// bool CWalletDB::LoadWallet() {
// bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew,
// bool SelectCoins(int64 nTargetValue, set<CWalletTx*>& setCoinsRet)

#[derive(Debug)]
pub struct Wallet {
    wallet_storage_with_cache: WalletStorageWithCache,

    consensus: Arc<Mutex<Consensus>>,
}

impl Wallet {
    pub async fn new(db_root_dir: &Path, consensus: Arc<Mutex<Consensus>>) -> Self {
        Self {
            wallet_storage_with_cache: WalletStorageWithCache::new(db_root_dir).await,
            consensus,
        }
    }
}

impl Wallet {
    pub async fn add_account(
        &mut self,
        public_key: &PublicKeyBytes,
        private_key: &PrivateKeyBytes,
    ) -> Result<(), WalletsError> {
        self.wallet_storage_with_cache
            .add_account(public_key, private_key)
            .await
    }

    pub async fn add_my_txs_in_block(&mut self, block: &Block) -> Result<(), WalletsError> {
        self.wallet_storage_with_cache
            .add_my_txs_in_block(block)
            .await
    }

    pub async fn get_balance(&self) -> TransactionValue {
        crate::get_balance(
            self.consensus.clone(),
            self.wallet_storage_with_cache.get_wallet_cache(),
        )
        .await
    }

    pub async fn select_coins(
        &self,
        target_value: TransactionValue,
        best_hight: u32,
    ) -> Result<Vec<WalletTx>, WalletsError> {
        self.wallet_storage_with_cache
            .select_coins(target_value, best_hight, self.consensus.clone())
            .await
    }

    pub fn get_wallet_cache(&self) -> &WalletCache {
        self.wallet_storage_with_cache.get_wallet_cache()
    }

    pub async fn add_tx_if_mine(
        &mut self,
        tx_id: &TransactionId,
        tx: &Transaction,
    ) -> Result<(), WalletsError> {
        self.wallet_storage_with_cache
            .add_tx_if_mine(tx_id, tx)
            .await
    }

    pub async fn commit_transaction(&mut self, tx: &Transaction) -> Result<(), WalletsError> {
        self.wallet_storage_with_cache.commit_transaction(tx).await
    }

    pub fn dump_wallet_account(&self) {
        self.wallet_storage_with_cache.dump_wallet_account();
    }

    pub async fn create_account(&mut self) -> Result<PublicKeyHashBytes, WalletsError> {
        self.wallet_storage_with_cache.create_account().await
    }

    pub async fn store(&self) -> Result<(), WalletsError> {
        self.wallet_storage_with_cache.store().await
    }

    pub async fn flush_database(&self) -> Result<(), WalletsError> {
        self.wallet_storage_with_cache.flush_database().await
    }

    pub fn exists_account(&self, public_key_hash: &PublicKeyHashBytes) -> bool {
        self.wallet_storage_with_cache
            .exists_account(public_key_hash)
    }

    pub fn exists_account_1(&self, public_key: &PublicKeyBytes) -> bool {
        self.wallet_storage_with_cache.exists_account_1(public_key)
    }

    pub fn is_tx_in_wallet(&self, tx_id: &TransactionId) -> bool {
        self.wallet_storage_with_cache.is_tx_in_wallet(tx_id)
    }

    pub fn get_my_pub_key_hash_from_tx_out(
        &self,
        tx_out: &TransactionOut,
    ) -> Result<Option<PublicKeyHashBytes>, WalletsError> {
        self.wallet_storage_with_cache
            .get_my_pub_key_hash_from_tx_out(tx_out)
    }

    pub fn get_pub_key(
        &self,
        pub_key_hash: &PublicKeyHashBytes,
    ) -> Result<PublicKeyBytes, WalletsError> {
        self.wallet_storage_with_cache.get_pub_key(pub_key_hash)
    }

    pub fn get_private_key(
        &self,
        public_key: &PublicKeyBytes,
    ) -> Result<PrivateKeyBytes, WalletsError> {
        self.wallet_storage_with_cache.get_private_key(public_key)
    }

    pub async fn get_item_number(&self) -> ItemNumberInWalletDb {
        self.wallet_storage_with_cache.get_item_number().await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    use scopeguard::defer;
    use tracing::debug;

    use bc_base::{
        PublicKeyHashBytes,
        constants::{
            ALICE_PRIVATE_KEY, ALICE_PUBLIC_KEY_HASH, ALICE_PUBLICK_KEY, HASH160_BYTES_LENGTH,
            PROOF_OF_WORK_LIMIT_NBITS, SUBSIDY,
        },
        transaction::TransactionOutPoint,
    };
    use bc_lib::top_storage::{
        BlockAndIndexStorageWithCache, MiscItemStorageWithCache,
        TransactionAndIndexStorageWithCache,
    };
    use bc_lib::{block, transaction};

    const NBITS: u32 = PROOF_OF_WORK_LIMIT_NBITS;

    async fn prepare(path: &PathBuf) -> Arc<Mutex<Consensus>> {
        debug!("db root dir: {:?}", path);

        let block_db = Arc::new(Mutex::new(BlockAndIndexStorageWithCache::new(path).await));
        let transaction_db = Arc::new(Mutex::new(
            TransactionAndIndexStorageWithCache::new(path).await,
        ));
        let misc_item_db = Arc::new(Mutex::new(MiscItemStorageWithCache::new(path).await));

        bc_lib::check_genesis_info_in_bc_storage(
            block_db.clone(),
            transaction_db.clone(),
            misc_item_db.clone(),
        )
        .await;
        let consensus = Consensus::new(block_db, transaction_db, misc_item_db).await;

        Arc::new(Mutex::new(consensus))
    }

    async fn prepare_wallet(
        path: &Path,
        consensus: Arc<Mutex<Consensus>>,
    ) -> (Wallet, PublicKeyBytes, PrivateKeyBytes) {
        let mut wallet = Wallet::new(path, consensus).await;
        let public_key = PublicKeyBytes::from_hex_str(ALICE_PUBLICK_KEY).unwrap();
        let private_key = PrivateKeyBytes::from_hex_str(ALICE_PRIVATE_KEY).unwrap();
        wallet.add_account(&public_key, &private_key).await.unwrap();
        (wallet, public_key, private_key)
    }

    #[tokio::test]
    async fn test_add_and_get_balance() {
        // bc_util::init_logger_for_test(None);
        let tmp_dir = bc_util::convert_to_temp_dir("test_add_and_get_balance");
        let path = tmp_dir.path().to_path_buf();
        let consensus = prepare(&path).await;
        defer! {
            tmp_dir.close().unwrap();
        };

        let (mut wallet, _, _) = prepare_wallet(&path, consensus.clone()).await;

        let pub_key_hash = PublicKeyHashBytes::from_hex_str(ALICE_PUBLIC_KEY_HASH).unwrap();
        let (_block_id, block, _tx) = block::create_block_for_test(&pub_key_hash, SUBSIDY, NBITS);

        let r = consensus.lock().await.process_block(&block).await;
        match r {
            Ok(blocks) => {
                for blk in &blocks {
                    wallet.add_my_txs_in_block(blk).await.unwrap();
                }
            }
            Err(e) => panic!("{:?}", e),
        }

        assert_eq!(wallet.get_balance().await, SUBSIDY);
    }

    fn create_tx_spend(
        tx_id: &TransactionId,
        payer_pub_key: &PublicKeyBytes,
        payer_priv_key: &PrivateKeyBytes,
        payer_pub_key_hash: &PublicKeyHashBytes,
    ) -> (TransactionId, Transaction) {
        let out_point_for_tx_in = TransactionOutPoint::new(*tx_id, 0);
        let tx_in =
            transaction::create_transaction_in(&out_point_for_tx_in, payer_pub_key, payer_priv_key);

        let payee_pub_key_hash_1 = PublicKeyHashBytes::new([1; HASH160_BYTES_LENGTH]);
        let tx_out1 = transaction::create_transaction_out(&payee_pub_key_hash_1, 600);
        let payee_pub_key_hash_2 = PublicKeyHashBytes::new([2; HASH160_BYTES_LENGTH]);
        let tx_out2 = transaction::create_transaction_out(&payee_pub_key_hash_2, 600);

        let tx_out3 = transaction::create_transaction_out(payer_pub_key_hash, SUBSIDY - 2 * 600);

        let tx_spend =
            transaction::create_transaction(vec![tx_in], vec![tx_out1, tx_out2, tx_out3]);
        let tx_id_spend = tx_spend.calc_tx_id();
        (tx_id_spend, tx_spend)
    }

    #[tokio::test]
    async fn test_spend_utxo() {
        // bc_util::init_logger_for_test(None);
        let tmp_dir = bc_util::convert_to_temp_dir("test_spend_utxo");
        let path = tmp_dir.path().to_path_buf();
        let consensus = prepare(&path).await;
        defer! {
            tmp_dir.close().unwrap();
        };

        let (mut wallet, payer_pub_key, payer_priv_key) =
            prepare_wallet(&path, consensus.clone()).await;

        // add utxo
        let payer_pub_key_hash = PublicKeyHashBytes::from_hex_str(ALICE_PUBLIC_KEY_HASH).unwrap();
        let (_block_id, block, tx) =
            block::create_block_for_test(&payer_pub_key_hash, SUBSIDY, NBITS);
        assert_eq!(payer_pub_key.to_public_key_hash(), payer_pub_key_hash);

        let r = consensus.lock().await.process_block(&block).await;
        match r {
            Ok(blocks) => {
                for blk in &blocks {
                    wallet.add_my_txs_in_block(blk).await.unwrap();
                }
            }
            Err(e) => panic!("{:?}", e),
        }

        assert_eq!(wallet.get_balance().await, SUBSIDY);

        // select utxo
        let coins = wallet.select_coins(600, 10).await.unwrap();
        assert_eq!(coins.len(), 1);
        assert_eq!(
            crate::get_credit(coins[0].get_tx(), wallet.get_wallet_cache(), consensus).await,
            SUBSIDY
        );

        let tx_id = tx.calc_tx_id();
        assert_eq!(coins[0].calc_tx_id(), tx_id);

        // spend
        let (tx_id_spend, tx_spend) =
            create_tx_spend(&tx_id, &payer_pub_key, &payer_priv_key, &payer_pub_key_hash);
        wallet
            .add_tx_if_mine(&tx_id_spend, &tx_spend)
            .await
            .unwrap();

        // commit tx
        wallet.commit_transaction(&tx_spend).await.unwrap();

        // check balance
        assert_eq!(wallet.get_balance().await, SUBSIDY - 2 * 600);
    }
}
